package com.yuanduo_app.bridge;

import android.Manifest;
import android.content.Context;
import android.content.pm.PackageManager;
import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.MediaRecorder;
import android.os.Build;
import android.os.Environment;
import android.util.Base64;
import android.util.Log;

import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.WritableMap;
import com.xujl.fastlib.base.BaseModule;
import com.xujl.fastlib.utils.LogS;
import com.xujl.fastlib.utils.StringUtils;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;

import androidx.core.content.ContextCompat;

public class AudioRecorderManager extends BaseModule {

    private static final String TAG = "ReactNativeAudio";

    private static final String DocumentDirectoryPath = "DocumentDirectoryPath";
    private static final String PicturesDirectoryPath = "PicturesDirectoryPath";
    private static final String MainBundlePath = "MainBundlePath";
    private static final String CachesDirectoryPath = "CachesDirectoryPath";
    private static final String LibraryDirectoryPath = "LibraryDirectoryPath";
    private static final String MusicDirectoryPath = "MusicDirectoryPath";
    private static final String DownloadsDirectoryPath = "DownloadsDirectoryPath";

    private Context context;
    private Recorder mRecorder;

    private String currentOutputFile;
    private boolean isRecording = false;
    private boolean isPaused = false;
    private boolean includeBase64 = false;
    private Timer timer;
    private StopWatch stopWatch;


    public AudioRecorderManager (ReactApplicationContext reactContext) {
        super(reactContext);
        this.context = reactContext;
        stopWatch = new StopWatch();


    }

    @Override
    public Map<String, Object> getConstants () {
        Map<String, Object> constants = new HashMap<>();
        constants.put(DocumentDirectoryPath, this.getReactApplicationContext().getFilesDir().getAbsolutePath());
        constants.put(PicturesDirectoryPath, Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).getAbsolutePath());
        constants.put(MainBundlePath, "");
        constants.put(CachesDirectoryPath, this.getReactApplicationContext().getCacheDir().getAbsolutePath());
        constants.put(LibraryDirectoryPath, "");
        constants.put(MusicDirectoryPath, Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC).getAbsolutePath());
        constants.put(DownloadsDirectoryPath, Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getAbsolutePath());
        return constants;
    }

    @Override
    public String getName () {
        return "AudioRecorderManager";
    }

    @ReactMethod
    public void checkAuthorizationStatus (Promise promise) {
        int permissionCheck = ContextCompat.checkSelfPermission(getCurrentActivity(),
                Manifest.permission.RECORD_AUDIO);
        boolean permissionGranted = permissionCheck == PackageManager.PERMISSION_GRANTED;
        promise.resolve(permissionGranted);
    }

    @ReactMethod
    public void prepareRecordingAtPath (String recordingPath, ReadableMap recordingSettings, Promise promise) {
        if (isRecording) {
            logAndRejectPromise(promise, "INVALID_STATE", "Please call stopRecording before starting recording");
        }
        includeBase64 = recordingSettings.getBoolean("IncludeBase64");
        final String audioEncoding = recordingSettings.getString("AudioEncoding");
        //wav格式需要使用另一种方式录制
        if (StringUtils.equals(audioEncoding, "wav")) {
            mRecorder = new HighQualityRecorder();
        } else {
            mRecorder = new NormalRecorder();
        }
        boolean isSuccess = mRecorder.init(recordingPath, recordingSettings, promise);
        if (!isSuccess) {
            return;
        }
        currentOutputFile = recordingPath;
        LogS.i("currentOutputFile", "" + currentOutputFile);
        promise.resolve(currentOutputFile);
    }


    @ReactMethod
    public void startRecording (Promise promise) {
        if (mRecorder == null) {
            logAndRejectPromise(promise, "RECORDING_NOT_PREPARED", "Please call prepareRecordingAtPath before starting recording");
            return;
        }
        if (isRecording) {
            logAndRejectPromise(promise, "INVALID_STATE", "Please call stopRecording before starting recording");
            return;
        }
        mRecorder.startRecorder();
        stopWatch.reset();
        stopWatch.start();
        isRecording = true;
        isPaused = false;
        startTimer();
        promise.resolve(currentOutputFile);
    }

    @ReactMethod
    public void stopRecording (Promise promise) {
        if (!isRecording) {
            logAndRejectPromise(promise, "INVALID_STATE", "Please call startRecording before stopping recording");
            return;
        }

        stopTimer();
        isRecording = false;
        isPaused = false;

        try {
            mRecorder.stop();
            stopWatch.stop();
        } catch (final RuntimeException e) {
            // https://developer.android.com/reference/android/media/MediaRecorder.html#stop()
            logAndRejectPromise(promise, "RUNTIME_EXCEPTION", "No valid audio data received. You may be using a device that can't record audio.");
            return;
        } finally {
            mRecorder.recycle();
        }

        promise.resolve(currentOutputFile);

        WritableMap result = Arguments.createMap();
        result.putString("status", "OK");
        result.putString("audioFileURL", "file://" + currentOutputFile);

        String base64 = "";
        if (includeBase64) {
            try {
                InputStream inputStream = new FileInputStream(currentOutputFile);
                byte[] bytes;
                byte[] buffer = new byte[8192];
                int bytesRead;
                ByteArrayOutputStream output = new ByteArrayOutputStream();
                try {
                    while ((bytesRead = inputStream.read(buffer)) != -1) {
                        output.write(buffer, 0, bytesRead);
                    }
                } catch (IOException e) {
                    Log.e(TAG, "FAILED TO PARSE FILE");
                }
                bytes = output.toByteArray();
                base64 = Base64.encodeToString(bytes, Base64.DEFAULT);
            } catch (FileNotFoundException e) {
                Log.e(TAG, "FAILED TO FIND FILE");
            }
        }
        result.putString("base64", base64);

        sendEvent("recordingFinished", result);
    }

    @ReactMethod
    public void pauseRecording (Promise promise) {


        if (!isPaused) {
            try {
                mRecorder.pauseRecording(promise);
                stopWatch.stop();
            } catch (Exception e) {
                e.printStackTrace();
                logAndRejectPromise(promise, "RUNTIME_EXCEPTION", "Method not available on this version of Android.");
                return;
            }
        }

        isPaused = true;
        promise.resolve(null);
    }

    @ReactMethod
    public void resumeRecording (Promise promise) {
        if (isPaused) {
            try {
                mRecorder.resumeRecording(promise);
                stopWatch.start();
            } catch (Exception e) {
                e.printStackTrace();
                logAndRejectPromise(promise, "RUNTIME_EXCEPTION", "Method not available on this version of Android.");
                return;
            }
        }

        isPaused = false;
        promise.resolve(null);
    }

    private void startTimer () {
        timer = new Timer();
        timer.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run () {
                if (!isPaused) {
                    WritableMap body = Arguments.createMap();
                    body.putDouble("currentTime", stopWatch.getTimeSeconds());
                    sendEvent("recordingProgress", body);
                }
            }
        }, 0, 1000);
    }

    private void stopTimer () {
        if (timer != null) {
            timer.cancel();
            timer.purge();
            timer = null;
        }
    }

    private static void logAndRejectPromise (Promise promise, String errorCode, String errorMessage) {
        Log.e(TAG, errorMessage);
        promise.reject(errorCode, errorMessage);
    }

    interface Recorder {
        boolean init (String recordingPath, ReadableMap recordingSettings, Promise promise);

        void startRecorder ();

        void resumeRecording (Promise promise) throws Exception;

        void pauseRecording (Promise promise) throws Exception;

        void recycle ();

        void stop ();
    }

    private static class NormalRecorder implements Recorder {
        private MediaRecorder recorder;
        private Method pauseMethod = null;
        private Method resumeMethod = null;
        private boolean isPauseResumeCapable = false;

        public NormalRecorder () {
            isPauseResumeCapable = Build.VERSION.SDK_INT > Build.VERSION_CODES.M;
            if (isPauseResumeCapable) {
                try {
                    pauseMethod = MediaRecorder.class.getMethod("pause");
                    resumeMethod = MediaRecorder.class.getMethod("resume");
                } catch (NoSuchMethodException e) {
                    Log.d("ERROR", "Failed to get a reference to pause and/or resume method");
                }
            }
        }

        @Override
        public boolean init (String recordingPath, ReadableMap recordingSettings, Promise promise) {
            File destFile = new File(recordingPath);
            if (destFile.getParentFile() != null) {
                destFile.getParentFile().mkdirs();
            }
            recorder = new MediaRecorder();
            try {
                recorder.setAudioSource(recordingSettings.getInt("AudioSource"));
                int outputFormat = getOutputFormatFromString(recordingSettings.getString("OutputFormat"));
                recorder.setOutputFormat(outputFormat);
                int audioEncoder = getAudioEncoderFromString(recordingSettings.getString("AudioEncoding"));
                recorder.setAudioEncoder(audioEncoder);
                recorder.setAudioSamplingRate(recordingSettings.getInt("SampleRate"));
                recorder.setAudioChannels(recordingSettings.getInt("Channels"));
                recorder.setAudioEncodingBitRate(recordingSettings.getInt("AudioEncodingBitRate"));
                recorder.setOutputFile(destFile.getPath());

            } catch (final Exception e) {
                logAndRejectPromise(promise, "COULDNT_CONFIGURE_MEDIA_RECORDER", "Make sure you've added RECORD_AUDIO permission to your AndroidManifest.xml file " + e.getMessage());
                return false;
            }
            try {
                recorder.prepare();
            } catch (final Exception e) {
                logAndRejectPromise(promise, "COULDNT_PREPARE_RECORDING_AT_PATH " + recordingPath, e.getMessage());
            }
            return true;
        }

        @Override
        public void startRecorder () {
            recorder.start();
        }

        @Override
        public void resumeRecording (Promise promise) throws Exception {
            if (!isPauseResumeCapable || resumeMethod == null) {
                logAndRejectPromise(promise, "RUNTIME_EXCEPTION", "Method not available on this version of Android.");
                throw new Exception("Method not available on this version of Android.");
            }
            resumeMethod.invoke(recorder);
        }

        @Override
        public void pauseRecording (Promise promise) throws Exception {
            if (!isPauseResumeCapable || pauseMethod == null) {
                logAndRejectPromise(promise, "RUNTIME_EXCEPTION", "Method not available on this version of Android.");
                throw new Exception("Method not available on this version of Android.");
            }
            pauseMethod.invoke(recorder);
        }

        @Override
        public void recycle () {
            recorder = null;
        }

        @Override
        public void stop () {
            recorder.stop();
            recorder.release();
        }

        private int getAudioEncoderFromString (String audioEncoder) {
            switch (audioEncoder) {
                case "aac":
                    return MediaRecorder.AudioEncoder.AAC;
                case "aac_eld":
                    return MediaRecorder.AudioEncoder.AAC_ELD;
                case "amr_nb":
                    return MediaRecorder.AudioEncoder.AMR_NB;
                case "amr_wb":
                    return MediaRecorder.AudioEncoder.AMR_WB;
                case "he_aac":
                    return MediaRecorder.AudioEncoder.HE_AAC;
                case "vorbis":
                    return MediaRecorder.AudioEncoder.VORBIS;
                default:
                    Log.d("INVALID_AUDIO_ENCODER", "USING MediaRecorder.AudioEncoder.DEFAULT instead of " + audioEncoder + ": " + MediaRecorder.AudioEncoder.DEFAULT);
                    return MediaRecorder.AudioEncoder.DEFAULT;
            }
        }

        private int getOutputFormatFromString (String outputFormat) {
            switch (outputFormat) {
                case "mpeg_4":
                    return MediaRecorder.OutputFormat.MPEG_4;
                case "aac_adts":
                    return MediaRecorder.OutputFormat.AAC_ADTS;
                case "amr_nb":
                    return MediaRecorder.OutputFormat.AMR_NB;
                case "amr_wb":
                    return MediaRecorder.OutputFormat.AMR_WB;
                case "three_gpp":
                    return MediaRecorder.OutputFormat.THREE_GPP;
                case "webm":
                    return MediaRecorder.OutputFormat.WEBM;
                default:
                    Log.d("INVALID_OUPUT_FORMAT", "USING MediaRecorder.OutputFormat.DEFAULT : " + MediaRecorder.OutputFormat.DEFAULT);
                    return MediaRecorder.OutputFormat.DEFAULT;

            }
        }

    }

    private static class HighQualityRecorder implements Recorder {
        private AudioRecord audioRecord;
        private RandomAccessFile raf;
        private int minBufferSize;
        private int sampleRate = 44100;
        private static final int STATE_STOP = 0;
        private static final int STATE_RECORD_ING = 1;//录音中
        private static final int STATE_RECORD_PAUSE = 2;//录音暂停中
        private int state = STATE_STOP;
        private final Object mLock = new Object();

        @Override
        public boolean init (String recordingPath, ReadableMap recordingSettings, Promise promise) {
            // 音源
            int audioSource = MediaRecorder.AudioSource.MIC;
            // 采样率
            sampleRate = 44100;
            // 声道数
            int channelConfig = AudioFormat.CHANNEL_IN_STEREO;//双声道
            // 采样位数
            int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
            // 获取最小缓存区大小
            minBufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat);
            // 创建录音对象
            audioRecord = new AudioRecord(audioSource, sampleRate, channelConfig, audioFormat, minBufferSize);
            try {
                if (!StringUtils.contains(recordingPath, ".wav")) {
                    raf = new RandomAccessFile(recordingPath + "/demo.wav", "rw");
                } else {
                    raf = new RandomAccessFile(recordingPath, "rw");
                }

            } catch (FileNotFoundException e) {
                e.printStackTrace();
            }
            return true;
        }

        @Override
        public void startRecorder () {
            // 耗时操作要开线程
            new Thread() {
                @Override
                public void run () {
                    try {
                        // 留出文件头的位置
                        raf.seek(44);
                        byte[] buffer = new byte[minBufferSize];

                        // 录音中
                        audioRecord.startRecording();
                        state = STATE_RECORD_ING;
                        while (state != STATE_STOP) {
                            if (state == STATE_RECORD_PAUSE) {
                                synchronized (mLock) {
                                    mLock.wait();
                                    continue;
                                }
                            }
                            int readSize = audioRecord.read(buffer, 0, minBufferSize);
                            raf.write(buffer, 0, readSize);
                        }

                        // 录音停止
                        audioRecord.stop();
                        audioRecord.release();
                        audioRecord = null;
                        // 写文件头
                        WriteWaveFileHeader(raf, raf.length(), sampleRate, 2, sampleRate * 16 * 2 / 8);
                        raf.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }.start();
        }

        @Override
        public void resumeRecording (Promise promise) throws Exception {
            state = STATE_RECORD_ING;
            synchronized (mLock) {
                mLock.notify();
            }
        }

        @Override
        public void pauseRecording (Promise promise) throws Exception {
            state = STATE_RECORD_PAUSE;
        }

        @Override
        public void recycle () {

        }

        @Override
        public void stop () {
            state = STATE_STOP;
        }

        /**
         * 为 wav 文件添加文件头，前提是在头部预留了 44字节空间
         *
         * @param raf        随机读写流
         * @param fileLength 文件总长
         * @param sampleRate 采样率
         * @param channels   声道数量
         * @param byteRate   码率 = 采样率 * 采样位数 * 声道数 / 8
         * @throws IOException
         */
        private void WriteWaveFileHeader (RandomAccessFile raf, long fileLength, long sampleRate, int channels, long byteRate) throws IOException {
            long totalDataLen = fileLength + 36;
            byte[] header = new byte[44];
            header[0] = 'R'; // RIFF/WAVE header
            header[1] = 'I';
            header[2] = 'F';
            header[3] = 'F';
            header[4] = (byte) (totalDataLen & 0xff);
            header[5] = (byte) ((totalDataLen >> 8) & 0xff);
            header[6] = (byte) ((totalDataLen >> 16) & 0xff);
            header[7] = (byte) ((totalDataLen >> 24) & 0xff);
            header[8] = 'W';
            header[9] = 'A';
            header[10] = 'V';
            header[11] = 'E';
            header[12] = 'f'; // 'fmt ' chunk
            header[13] = 'm';
            header[14] = 't';
            header[15] = ' ';
            header[16] = 16; // 4 bytes: size of 'fmt ' chunk
            header[17] = 0;
            header[18] = 0;
            header[19] = 0;
            header[20] = 1; // format = 1
            header[21] = 0;
            header[22] = (byte) channels;
            header[23] = 0;
            header[24] = (byte) (sampleRate & 0xff);
            header[25] = (byte) ((sampleRate >> 8) & 0xff);
            header[26] = (byte) ((sampleRate >> 16) & 0xff);
            header[27] = (byte) ((sampleRate >> 24) & 0xff);
            header[28] = (byte) (byteRate & 0xff);
            header[29] = (byte) ((byteRate >> 8) & 0xff);
            header[30] = (byte) ((byteRate >> 16) & 0xff);
            header[31] = (byte) ((byteRate >> 24) & 0xff);
            header[32] = (byte) (2 * 16 / 8); // block align
            header[33] = 0;
            header[34] = 16; // bits per sample
            header[35] = 0;
            header[36] = 'd';
            header[37] = 'a';
            header[38] = 't';
            header[39] = 'a';
            header[40] = (byte) (fileLength & 0xff);
            header[41] = (byte) ((fileLength >> 8) & 0xff);
            header[42] = (byte) ((fileLength >> 16) & 0xff);
            header[43] = (byte) ((fileLength >> 24) & 0xff);
            raf.seek(0);
            raf.write(header, 0, 44);
        }
    }

    private static class StopWatch {
        private long start;
        private float elapsedTime = 0;
        private boolean paused = true;

        public StopWatch () {
        }

        public void start () {
            start = System.currentTimeMillis();
            paused = false;
        }

        public float stop () {
            if (!paused) {
                long now = System.currentTimeMillis();
                elapsedTime += (now - start) / 1000f;
                paused = true;
            }

            return elapsedTime;
        }

        public void reset () {
            start = 0;
            elapsedTime = 0;
            paused = true;
        }

        public float getTimeSeconds () {
            float seconds;

            if (paused) {
                seconds = elapsedTime;
            } else {
                long now = System.currentTimeMillis();
                seconds = elapsedTime + (now - start) / 1000f;
            }
            return seconds;
        }
    }
}
